#!/usr/bin/env python3
#
# Copyright 2022 Petter Reinholdtsen
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA.

"""

LinuxCNC automated PID auto-tuner
=================================

Automatically conduct a automatic PID tuning using the pid
LinuxCNC component.

This script first home the axis, then move it to the middle of its range.
Also extend the f-error value used by the PID controller.  Next, disable
the PID controller (tune-mode=1), start the tuning process (tune-start=1)
and wait for it to finish before enabling the PID controller again.

This script assume the pid component is configured like this:

  loadrt pid names=pid.x,pid.y,pid.z

It also assume the axis ranges are correct to be able to position the axes
in their middle positions.
"""

import time

import linuxcnc
import linuxcnc_util
import hal

axis = 'x'
axisid = 0

# Some values that need to be replaced during tuning
replace = {
#    f"pid.{axis}.tune-cycles": "0x40",  # Increase number of back and forth movement (default 50)
    f"pid.{axis}.tune-effort": "0.2",  # Set effort, should start low
    f"ini.{axisid}.ferror": "100",     # Extend to not trigger following error
    f"ini.{axisid}.min_ferror": "100", # Extend to not trigger following error
    }

def set_mode(mode):
    s.poll()
    if linuxcnc.MODE_MDI != s.task_mode:
        c.mode(linuxcnc.MODE_MDI)
        c.wait_complete()
        s.poll()
def is_homed():
    isHomed=True
    for i,h in enumerate(s.homed):
        if i >= s.joints: break
        isHomed = isHomed and h
    return isHomed

c = linuxcnc.command()
s = linuxcnc.stat();
e = linuxcnc.error_channel()
l = linuxcnc_util.LinuxCNC(command=c, status=s, error=e)

s.poll()

# Bootstrap hal library
hc = hal.component("hal-watcher")

error = []

# Make sure machine is turned on and out of estop
if linuxcnc.STATE_ESTOP == s.task_state or linuxcnc.STATE_OFF == s.task_state:
    error.append("error: You need to start the machine and make sure ESTOP is not active")

effortpin = f"pid.{axis}.tune-effort"
try:
    v = hal.get_value(effortpin)
except RuntimeError as e:
    error.append(f"error: failed to find at_pin specific pin {effortpin} in HAL")
    error.append(f"error: Did you remember to replace 'loadrt pid' with 'loadrt at_pid' for axis {axis}?")

if error:
    print("\n".join(error))
    exit(1)

# Start homing, wait for it to complete
set_mode(linuxcnc.MODE_MANUAL)
if not is_homed():
    print("Requesting axis homing.")
    c.teleop_enable(0)
    c.wait_complete()
    c.home(-1)
    c.wait_complete()
    l.wait_for_home([1, 1, 1, 0, 0, 0, 0, 0, 0])
else:
    print("Already homed, skipping axis homing.")

# Locate mid range and move to the middle, switch to MDI mode while moving

midpos = (
    s.joint[axisid]["max_position_limit"]
    + s.joint[axisid]["min_position_limit"]
) / 2
print(f"Moving {axis} to position {midpos}")
set_mode(linuxcnc.MODE_MDI)
c.mdi(f"G0 {axis}{midpos}")
c.wait_complete()
set_mode(linuxcnc.MODE_MANUAL)


print("info: Will start PID auto tuning in 5 seconds")
time.sleep(5)

# Replace some old values
oldvals = {}
for pin in replace.keys():
    print(f"Keeping {pin}")
    oldvals[pin] = hal.get_value(pin)
    hal.set_p(pin, replace[pin])

# Run PID autotuning
print("Starting PID tuning")
hal.set_p(f"pid.{axis}.tune-mode", "1")
hal.set_p(f"pid.{axis}.tune-start", "1")

startpin = f"pid.{axis}.tune-start"
tunerunning = hal.get_value(startpin)
while hal.get_value(startpin) == tunerunning:
    time.sleep(0.1)
hal.set_p(f"pid.{axis}.tune-mode", "0")
print(f"PID tuning of axis {axis} completed.")

# Give axis time to return to where it want to be before restoring old
# error limits.  Reissuing G0 from above to be able to use
# wait_complete() to detect when it is done.
set_mode(linuxcnc.MODE_MDI)
c.mdi(f"G0 {axis}{midpos}")
c.wait_complete()
set_mode(linuxcnc.MODE_MANUAL)

# Reinsert old values
for pin in replace.keys():
    hal.set_p(pin, "%s" % oldvals[pin])
